#pragma once
#include <cstdlib>
#include <type_traits>
#include <exception>
#include <iostream>
#include <memory>

class AllocatorObject
{
  public:
    using size_type = std::size_t;

    void* allocate(size_type size)
    {
        if (auto ptr = malloc(size))
        {
            std::cout << "AllocatorObject " << this << " allocates " << ptr
                      << " with size = " << size << "\n";
            return ptr;
        }
        throw std::bad_alloc{};
    }
    void deallocate(void* ptr)
    {
        std::cout << "AllocatorObject " << this << " deallocates " << ptr
                  << '\n';
        free(ptr);
    }
};

template <typename T>
class MyAllocator
{
  public:
    using value_type = T;
    using pointer = T*;
    using size_type = std::size_t;
    using is_always_equal = std::false_type;
    using propagate_on_container_copy_assignment = std::true_type;
    using propagate_on_container_move_assignment = std::true_type;
    using propagate_on_container_swap = std::true_type;

    MyAllocator() : object_(std::make_shared<AllocatorObject>())
    {
        std::cout << "MyAllocator " << this
                  << " constructor, sizeof(T): " << sizeof(T) << '\n';
    }

    template <typename U>
    MyAllocator(const MyAllocator<U>& other) noexcept : object_(other.object_)
    {
        std::cout << "MyAllocator " << this
                  << " copy constructor(U), sizeof(T): " << sizeof(T) << '\n';
    }

    MyAllocator(MyAllocator&& other) noexcept
        : object_(std::move(other.object_))
    {
        std::cout << "MyAllocator " << this
                  << " move constructor, sizeof(T): " << sizeof(T) << '\n';
    }

    MyAllocator& operator=(MyAllocator&& other) noexcept
    {
        std::cout << "MyAllocator " << this
                  << " move assignment, sizeof(T): " << sizeof(T) << '\n';
        object_ = std::move(other.object_);
        return *this;
    }

    MyAllocator(const MyAllocator& other) noexcept : object_(other.object_)
    {
        std::cout << "MyAllocator " << this
                  << " copy constructor, sizeof(T): " << sizeof(T) << '\n';
    }

    MyAllocator& operator=(const MyAllocator& other) noexcept
    {
        std::cout << "MyAllocator " << this
                  << " copy assignment, sizeof(T): " << sizeof(T) << '\n';
        object_ = other.object_;
        return *this;
    }

    pointer allocate(size_type n)
    {
        auto ptr = object_->allocate(sizeof(T) * n);
        return static_cast<pointer>(ptr);
    }

    void deallocate(pointer ptr, size_type n)
    {
        (void)n;
        return object_->deallocate(ptr);
    }

  private:
    std::shared_ptr<AllocatorObject> object_;

    template <typename T1, typename T2>
    friend bool operator==(const MyAllocator<T1>& lhs,
                           const MyAllocator<T2>& rhs);

    template <typename T1, typename T2>
    friend bool operator!=(const MyAllocator<T1>& lhs,
                           const MyAllocator<T2>& rhs);

    template <typename U>
    friend class MyAllocator;
};

template <typename T1, typename T2>
bool operator==(const MyAllocator<T1>& lhs, const MyAllocator<T2>& rhs)
{
    return lhs.object_.get() == rhs.object_.get();
}

template <typename T1, typename T2>
bool operator!=(const MyAllocator<T1>& lhs, const MyAllocator<T2>& rhs)
{
    return lhs.object_.get() == rhs.object_.get();
}